#include "Merger.h"
#include "../utils/FileSystem.h"
#include "../Test.h"
#include <math.h>
#include <sstream>


Merger::Merger(const int cantParticiones, std::string carpetaParticiones) {
	this->_cantArchivosExistentes = cantParticiones;
	this->_carpetaParticiones = carpetaParticiones;
	this->_ordenParticionInit = 1;
}


Merger::~Merger() {
	this->_cantArchivosExistentes = 0;
	this->_carpetaParticiones = "";
	this->_ordenParticionInit = 1;
	descargarArchivos();
}


/**
 * Fusiona "fusionesSimultaneas" cantidad de particiones a la vez
 * y devuelve el nombre del archivo final
 * "incluirOrden" indica si el archivo final debe contener los identificadores de orden
 */
std::string Merger::fusionarParticiones(int fusionesSimultaneas, bool incluirOrden){
	if (fusionesSimultaneas < 2)
		throw std::runtime_error("No se pueden fusionar menos de 2 archivos");

	// Evita un problema si se trata de ingresar una cantidad muy alta
	if (fusionesSimultaneas > this->_cantArchivosExistentes){
		fusionesSimultaneas = this->_cantArchivosExistentes;
	}

	// Redondea hacia arriba la cantidad de etapas
	int cantidadEtapas = calcularCantEtapas(fusionesSimultaneas, this->_cantArchivosExistentes);

	// Ejecuta el proceso de cada etapa
	for (int etapa = 1; etapa <= cantidadEtapas; ++etapa){
		FileSystem::crearCarpetaFusion(etapa);
		int cantDeArchivosParciales = 0;	// En la etapa actual

		// Mientras haya particiones para procesar en la etapa
		while (this->_ordenParticionInit <= this->_cantArchivosExistentes){

			// Para las fusiones simultaneas
			cargarArchivos(fusionesSimultaneas, etapa);

			// Fusiona todos los archivos
			cantDeArchivosParciales++;
			fusionarArchivos(cantDeArchivosParciales, etapa);

			descargarArchivos();
		}

//		Test::leerFusion(cantDeArchivosParciales, etapa);

		// Inicializa la siguiente etapa
		this->_ordenParticionInit = 1;
		this->_cantArchivosExistentes = cantDeArchivosParciales;
	}

	// Copia el archivo resultado a la raiz con o sin los identificadores
	std::string nomArch = moverArchResultado(cantidadEtapas, incluirOrden);
	return nomArch;
}


// Realiza la fusion de todos los archivos cargados en el vector de particiones
void Merger::fusionarArchivos(int nroFusion, int etapa){

	// Inicializa el vector de terminos
	std::vector<PalabraAleatoria> vecTerminos;
	leerTerminos(vecTerminos);

	// Abre el archivo de salida
	std::string nomArchSalida = FileSystem::getNombreFusion(nroFusion, etapa);
	this->fout.open(nomArchSalida.c_str(),
			std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);

	PalabraAleatoria terminoAnterior;
	while ( existenTerminos(vecTerminos) ){

		// Actualiza el vector de terminos leyendo las particiones
		leerTerminos(vecTerminos, terminoAnterior);

		// Busca el menor de los terminos y lo guarda en el archivo de salida
		PalabraAleatoria terminoMin = getTerminoMinimo(vecTerminos);
		terminoMin.guardar(&this->fout);

		// Lleva cuenta del termino que se guardó para saber cual partición leer
		terminoAnterior = terminoMin;
	}

	// Cierra el archivo de salida
	this->fout.close();
}


// Carga la cantidad de particiones en el vector para realizar la fusion
void Merger::cargarArchivos(int cantArch, int etapa){

	// Para que carge la cantidad correcta de archivos con el numero de orden correcto
	int nroArchFinal = cantArch + this->_ordenParticionInit -1;
	if (nroArchFinal > this->_cantArchivosExistentes){
		nroArchFinal = this->_cantArchivosExistentes;
	}

	// Cuidado que no trate de cargar mas archivos de los que existen
	for (int i = this->_ordenParticionInit; i <= nroArchFinal; i++){

		// Abre el archivo a procesar
		std::string nomArch;
		if (etapa == 1){
			nomArch = this->_carpetaParticiones + "/" + FileSystem::getNombreParticion(i);
		} else {
			// Porque "-1"? porque estoy en la etapa "x" pero necesito los archivos de la etapa "x-1"
			nomArch = FileSystem::getNombreFusion(i, etapa-1);
		}

		std::ifstream* fin = new std::ifstream(nomArch.c_str(), std::ifstream::in | std::ifstream::binary);

		if (!fin) throw std::runtime_error("Error al abrir el archivo " +nomArch);

		this->_vecArchEntrada.push_back(fin);
	}

	// Actualiza cual es el nro de la proxima particion inicial
	this->_ordenParticionInit += cantArch;
}


void Merger::descargarArchivos(){
	std::vector<std::ifstream*>::iterator it;
	for (it = this->_vecArchEntrada.begin(); it != this->_vecArchEntrada.end(); ++it){

		std::ifstream& fin = **it;

		if (fin.is_open()) fin.close();

		delete *it;
		*it = NULL;
	}

	// Lo deja cero km
	this->_vecArchEntrada.clear();
}


bool Merger::existenTerminos(std::vector<PalabraAleatoria> &terminos) const{
	std::vector<PalabraAleatoria>::const_iterator cit;
	for (cit = terminos.begin(); cit != terminos.end(); ++cit){
		// Existe al menos un termino menor que el maximo imposible
		if ( (*cit).getIdentificador() < PalabraAleatoria::MAXIMO_IMPOSIBLE()){
			return true;
		}
	}

	return false;
}


// Actualiza el vector de terminos leyendo del archivo que corresponde
// según cual haya sido el anterior termino leido
void Merger::leerTerminos(std::vector<PalabraAleatoria> &terminos, PalabraAleatoria termAnterior){

	// Evita un problema al inicio del proceso
	if (termAnterior.getIdentificador() == PalabraAleatoria::MAXIMO_IMPOSIBLE()){
		// Esto pasa porque recien empezó o porque ya terminó, en los dos casos no hace nada
		return;
	}

	// Busco en el vector de terminos la posicion del termino que fue leido con anterioridad
	unsigned int pos;
	for (pos = 0; pos < terminos.size(); ++pos){
		if (terminos[pos].getIdentificador() == termAnterior.getIdentificador()){
			// Encontré la posicion del termino que fue leido anteriormente
			break;
		}
	}

	// Leo el archivo que corresponde a ese termino
	std::ifstream* pFin = this->_vecArchEntrada[pos];
	PalabraAleatoria p;
	p.cargarPalabra(*pFin);

	// Guardo el termino leido en el vector
	terminos[pos] = p;
}


// Inicializa el vector de terminos
void Merger::leerTerminos(std::vector<PalabraAleatoria> &terminos){
	// Recupera una palabra de cada archivo
	std::vector<std::ifstream*>::iterator it;
	for (it = this->_vecArchEntrada.begin(); it != this->_vecArchEntrada.end(); ++it){
		PalabraAleatoria p;
		p.cargarPalabra(**it);
		terminos.push_back(p);
	}
}


// Recorre todo el vector hasta encontrar la menor de las palabras
PalabraAleatoria Merger::getTerminoMinimo(std::vector<PalabraAleatoria> &terminos) const{

	std::vector<PalabraAleatoria>::const_iterator cit;
	std::vector<PalabraAleatoria>::const_iterator citMin = terminos.begin();

	for (cit = terminos.begin(); cit != terminos.end(); ++cit){
		if ( (*cit).getIdentificador() < (*citMin).getIdentificador() ){
			// Encontré un nuevo valor minimo
			citMin = cit;
		}
	}

	// Devuelve una copia del minimo termino encontrado
	return *citMin;
}


// Copia el resultado a la raiz del programa, puede o no incluir el orden de los terminos
std::string Merger::moverArchResultado(int etapa, bool incluirOrden) const{
	std::string archOrigen = FileSystem::getNombreFusion(1, etapa);
	std::string archDestino = "diccionarioDesordenado.bin";

	// Abre los archivos
	std::ifstream fin(archOrigen.c_str(), std::ifstream::in | std::ofstream::binary);
	std::ofstream fout(archDestino.c_str(), std::ofstream::out | std::ofstream::binary | std::ofstream::trunc);

	while (!fin.eof()){
		PalabraAleatoria p;
		p.cargarPalabra(fin);

		if (!fin.eof()){
			p.guardar(&fout);
		}
	}

	fin.close();
	fout.close();

	return archDestino;
}


int Merger::calcularCantEtapas(int nivelMerge, int cantTotalArchivos) const{
	// Para no tener problemas con las fracciones
	float nivel = nivelMerge;

	int cantEtapas = 0;
	int resul = cantTotalArchivos;

	while (resul != 1){
		resul = ceil( resul / nivel );
		cantEtapas++;
	}

	return cantEtapas;
}
